package boardgamer.yavalath.client.replay;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Pair;
import android.view.MotionEvent;
import boardgamer.common.GameUpdate;
import boardgamer.common.client.animation.AnimationCollection;
import boardgamer.common.client.animation.Screen;
import boardgamer.common.client.animation.WaitAnimation;
import boardgamer.common.client.layout.LayoutGroup;
import boardgamer.common.net.PlayerId;
import boardgamer.yavalath.client.animation.FadingBoardAnimation;
import boardgamer.yavalath.client.animation.HeaderAnimation;
import boardgamer.yavalath.client.animation.LoseAnimation;
import boardgamer.yavalath.client.animation.MoveAnimation;
import boardgamer.yavalath.client.game.YavalathBoardLayout;
import boardgamer.yavalath.client.game.YavalathDrawableBoard;
import boardgamer.yavalath.client.game.YavalathDrawableTile;
import boardgamer.yavalath.client.resources.SoundHandler;
import boardgamer.yavalath.client.resources.StringHandler;
import boardgamer.yavalath.common.YavalathGameState;
import boardgamer.yavalath.common.YavalathGameStateChange;
import boardgamer.yavalath.common.YavalathPlayerState;
import boardgamer.yavalath.common.YavalathReplay;
import boardgamer.yavalath.common.YavalathTile;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

import java.util.Set;

import static boardgamer.yavalath.client.game.GameView.HEADER_SCREEN_HEIGHT_RATIO;
import static boardgamer.yavalath.client.game.GameView.HEADER_SCREEN_WIDTH_RATIO;
import static boardgamer.yavalath.client.replay.ReplayControl.Controllable;
import static boardgamer.yavalath.client.replay.ReplayPlayer.NextTimeHandler;
import static boardgamer.yavalath.common.YavalathGameStateChange.PlayerAddToken;
import static boardgamer.yavalath.common.YavalathGameStateChange.PlayerLoses;
import static boardgamer.yavalath.common.YavalathGameStateChange.PlayerWins;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * @author garysoed
 */
public class ReplayScreen implements Screen, NextTimeHandler, Controllable {

  private final YavalathReplay replay;
  private final YavalathDrawableBoard board;
  private final YavalathBoardLayout boardLayout;
  private final AnimationCollection animations;
  private final ReplayPlayer replayPlayer;
  private final ReplayControl control;
  private HeaderAnimation headerAnimation;
  private int index;

  public ReplayScreen(YavalathReplay replay) {
    this.replay = checkNotNull(replay, "replay");

    YavalathGameState initialGameState = replay.getInitialGameState();
    this.boardLayout = new YavalathBoardLayout();
    this.board = new YavalathDrawableBoard(
        new YavalathDrawableTile.Factory(initialGameState.getPlayerStates(), 0 /* startAlpha */),
        initialGameState.getBoard());
    this.boardLayout.setBoard(board);
    this.animations = new AnimationCollection();
    this.replayPlayer = new ReplayPlayer(this);
    this.control = new ReplayControl(replayPlayer, this);
  }

  @Override
  public void initialize(Rect canvasRect) {
    this.headerAnimation = new HeaderAnimation(
        canvasRect.width() * HEADER_SCREEN_WIDTH_RATIO,
        canvasRect.height() * HEADER_SCREEN_HEIGHT_RATIO);
    
    new LayoutGroup()
        .add(control).atCurrentRect()
        .setBoundingRect(new RectF(canvasRect));

    new LayoutGroup()
        .add(boardLayout).atCurrentRect()
        .setBoundingRect(new RectF(canvasRect));

    float gap = (canvasRect.height()
        - boardLayout.getBoundingRect().height()
         - headerAnimation.getBoundingRect().height()
        - control.getBoundingRect().height()) / 3;
    new LayoutGroup()
        .add(boardLayout).atCurrentRect()
        .add(headerAnimation).relativeToEdge(boardLayout, LayoutGroup.Direction.BOTTOM, gap)
        .add(control).relativeToEdge(headerAnimation, LayoutGroup.Direction.BOTTOM, gap * 2)
        .horizontalAlign(LayoutGroup.HorizontalAlign.CENTER)
        .setBoundingRect(new RectF(canvasRect));
  }

  private void setReplayIndex(int index) {
    this.index = index;
    
    GameUpdate<YavalathGameState, YavalathGameStateChange> update = replay.getChange(index);
    // If not the first change, update the game state to the previous one
    if (index > 0) {
      YavalathGameState state = replay.getChange(index - 1).getEndState();
      board.update(state.getBoard(), true /* resetAlpha */);
      board.setLosers(state.getLosers());
    }

    if (update.getStateChange() != null) {
      YavalathGameStateChange change = update.getStateChange();
      switch (change.getEvent()) {
        case ADD_TOKEN:
          PlayerAddToken addTokenChange = (PlayerAddToken) change;
          board.getTile(addTokenChange.getX(), addTokenChange.getY())
              .setOwner(addTokenChange.getPlayer());
          animations.addTemporary(
              new MoveAnimation(board, addTokenChange.getX(), addTokenChange.getY()));
          SoundHandler.playWave(SoundHandler.moveSound);
          
          displayNextPlayer(update.getEndState());
          break;

        case DRAW:
          headerAnimation.display(StringHandler.instance().gameDrawMessage, Color.WHITE);
          break;
        
        case JOIN_GAME:
          throw new UnsupportedOperationException("Should not have join game updates");
        case START_GAME:
          break;
        case PLAYER_LOSES:
          final PlayerLoses loseStateChange = (PlayerLoses) change;
          final Set<Pair<Integer, Integer>> losingTiles
              = ImmutableSet.copyOf(Iterables.transform(loseStateChange.getLosingTiles(),
              new Function<YavalathTile, Pair<Integer, Integer>>() {
                @Override
                public Pair<Integer, Integer> apply(YavalathTile input) {
                  return Pair.create(input.getX(), input.getY());
                }
              }));
          final PlayerId loser = loseStateChange.getPlayer();
          animations
              .addBlocking(FadingBoardAnimation.forDisappearingBoard(
                  new Predicate<YavalathTile>() {
                    @Override
                    public boolean apply(YavalathTile input) {
                      return !losingTiles.contains(Pair.create(input.getX(), input.getY()))
                          && input.hasOwner()
                          && input.getOwner() == loser;
                    }
                  }, // notFadingPredicate
                  board))
              .addBlocking(new WaitAnimation(1000),
                  new AnimationCollection.DeactivateHandler() {
                    @Override
                    public void onDeactivate() {
                      board.addLoser(loseStateChange.getPlayer());
                      YavalathPlayerState.PlayerColor loserColor
                          = replay.getInitialGameState().getPlayerStates().get(loser).getColor();
                      animations.addTemporary(new LoseAnimation(losingTiles, loserColor, board));
                    }
                  });
          break;

        case PLAYER_WINS:
          PlayerWins winChange = (PlayerWins) change;
          final PlayerId winner = winChange.getPlayer();
          animations
              .addBlocking(FadingBoardAnimation.forDisappearingBoard(
                  new Predicate<YavalathTile>() {
                    @Override
                    public boolean apply(YavalathTile input) {
                      return !input.hasOwner() || input.getOwner() != winner;
                    }
                  }, // fadingPredicate
                  board));
          headerAnimation.display(
              StringHandler.instance().getWinMessage(winner.getName()),
              replay.getInitialGameState().getPlayerStates().get(winner).getColor());
          break;
      }
    }
    
    board.update(update.getEndState().getBoard());
  }

  @Override
  public void onOpen() {
    animations.addBlocking(FadingBoardAnimation.forAppearingBoard(board),
        new AnimationCollection.DeactivateHandler() {
          @Override
          public void onDeactivate() {
            displayNextPlayer(replay.getInitialGameState());
            control.setPlaying(true);
          }
        });
    animations.addTemporary(headerAnimation);
  }

  @Override
  public void onResume() { }

  @Override
  public void onUpdate(int elapsedTime) {
    animations.update(elapsedTime);
    replayPlayer.update(elapsedTime);
  }

  @Override
  public void onDraw(Canvas canvas) {
    board.draw(canvas);
    control.draw(canvas);
    animations.draw(canvas);
  }

  @Override
  public void onTouchEvent(MotionEvent event) {
    control.handleEvent(event.getX(), event.getY(), event.getAction());
  }

  @Override
  public void handleNextTime() {
    forwardOne();
  }

  private void displayNextPlayer(YavalathGameState newState) {
    PlayerId nextPlayer = newState.getCurrentPlayer();
    YavalathPlayerState.PlayerColor nextColor
        = replay.getInitialGameState().getPlayerStates().get(nextPlayer).getColor();
    headerAnimation.display(newState.getCurrentPlayer().getName() + "'s turn", nextColor);
  }

  @Override
  public void begin() {
    setReplayIndex(1);
  }

  @Override
  public void backOne() {
    if (index > 1) {
      setReplayIndex(index - 1);
    }
  }

  @Override
  public void forwardOne() {
    if (index < replay.getChangeCount() - 1) {
      setReplayIndex(index + 1);
    }
  }

  @Override
  public void end() {
    setReplayIndex(replay.getChangeCount() - 1);
  }
}
